個人覺得 React Native 內建的 Modal 組件對初學者來說非常不友好,因為需要多做很多額外的處理,大部分時候我都會選擇使用 react-native-modal 或者 UI library 的 Modal 而不是 React Native 內建的 Modal,但還是要搞明白內建 Modal 最基本的使用方式
transparent
設為 truebackgroundColor: 'rgba(0,0,0,.5)'
這是一個 Modal 的基本寫法:
import { StyleSheet, Modal, View, Text, ModalProps } from "react-native"
// ...
export const CustomModal = ({ visible, onRequestClose }: ModalProps) => {
return (
<Modal
animationType="fade"
transparent={true}
visible={visible}
onRequestClose={onRequestClose}
>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text>Modal</Text>
</View>
</View>
</Modal>
)
}
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,.5)'
},
modalView: {
width: 300,
height: 350,
margin: 20,
backgroundColor: 'white',
borderRadius: 5,
padding: 35,
alignItems: 'center'
},
})
如果是上面的例子,點擊外面是無法關閉 Modal 的,這是因為 centeredView 其實占了整個畫面,因此也算是 Modal 的一部分,點擊 Modal 內部當然是無法關閉 modal 的。
如果要實現點擊 Modal 外部關閉 Modal 的話,可以將 TouchableWithoutFeedback
組件包裹在 Modal 中:
<Modal
animationType="fade"
transparent={true}
visible={visible}
>
<TouchableWithoutFeedback onPress={onRequestClose}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text>Modal</Text>
</View>
</View>
</TouchableWithoutFeedback>
</Modal>
但這樣做的話不管是點 Modal 內部還是外部都一樣會關閉 Modal,所以我們需要判斷點擊的是否為 Modal 外部,如果是才關閉:
const onBackdropPress = (event: GestureResponderEvent) => {
if (event.target === event.currentTarget) {
onRequestClose && onRequestClose(event)
return
}
}
event.target
: 用戶實際點擊或操作的那個元素。event.currentTarget
: 當前正在處理事件的元素,可以理解為事件目前正在"冒泡到"的元素。event.target === event.currentTarget
用於判斷是否點擊的是 Modal 外部。import { StyleSheet, Modal, View, Text, TouchableWithoutFeedback, ModalProps, GestureResponderEvent } from "react-native"
export const CustomModal = ({ visible, onRequestClose }: ModalProps) => {
const onBackdropPress = (event: GestureResponderEvent) => {
if (event.target === event.currentTarget) {
onRequestClose && onRequestClose(event)
return
}
}
return (
<Modal
animationType="fade"
transparent={true}
visible={visible}
>
<TouchableWithoutFeedback onPress={onBackdropPress}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text>Modal</Text>
</View>
</View>
</TouchableWithoutFeedback>
</Modal>
)
}
當 Modal 中的內容過長時 Modal 會無限增長直到超出屏幕的高度:
要解決也很簡單,直接限制 Modal 高度然後加個 ScrollView 就行
(如果設 maxHeight 就是只有超過這個高度才會需要滾動)
import { StyleSheet, Modal, ScrollView, View, Text, TouchableWithoutFeedback, ModalProps, GestureResponderEvent } from "react-native"
export const CustomModal = ({ visible, onRequestClose }: ModalProps) => {
const onBackdropPress = (event: GestureResponderEvent) => {
if (event.target === event.currentTarget) {
onRequestClose && onRequestClose(event)
return
}
}
return (
<Modal
animationType="fade"
transparent={true}
visible={visible}
>
<TouchableWithoutFeedback onPress={onBackdropPress}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<ScrollView>
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</Text>
</ScrollView>
</View>
</View>
</TouchableWithoutFeedback>
</Modal>
)
}
有些頁面需要控制多個 Modal 或者有些 Modal 需要在多個頁面被使用,如果每個頁面都寫上 Modal 相關的程式碼那就會變得十分臃腫,所以可以把 Modal 抽象出來當作全局組件。
最簡單的實現全局 Modal 的方式是使用 Context + Provider。
我們可以寫兩個方法供全局控制Modal的開關:
openModal
: 給定 Modal 名稱,彈出指定 ModalcloseModal
: 關閉當前 Modal,初始化狀態// _types_/modal.ts
export interface ModalContextType {
openModal(key: string): void
closeModal(): void
}
新增 Context 和 Provider:
visible
: Modal 彈出狀態modalType
: 決定顯示哪個 Modal// context/modalContext.ts
import { createContext } from "react"
import { ModalContextType } from "_types_"
export const ModalContext = createContext<ModalContextType>({
openModal: () => {},
closeModal: () => {},
})
// provider/ModalProvider.tsx
import React, { useState, useMemo, useCallback } from "react"
import { CustomModal, DeleteModal } from "@/components/organisms"
import { ModalContext } from "@/context"
const MODAL_COMPONENTS = {
default: CustomModal
delete: DeleteModal
}
export interface ModalProviderProps {
children: React.ReactNode
}
export const ModalProvider = ({ children }: ModalProviderProps) => {
const [visible, setVisible] = useState(false)
const [modalType, setModalType] = useState<string>('')
const openModal = useCallback((key: string) => {
setVisible(true)
setModalType(key)
}, [])
const closeModal = useCallback(() => {
setVisible(false)
setModalType('')
}, [])
const contextValue = useMemo(() => ({ openModal, closeModal }), [])
const renderModal = () => {
const ModalContent = MODAL_COMPONENTS[modalType as keyof typeof MODAL_COMPONENTS] ?? MODAL_COMPONENTS.default
if (!modalType || !ModalContent) return null
return <ModalContent visible={visible} onRequestClose={closeModal} />
}
return (
<ModalContext.Provider value={contextValue}>
{renderModal()}
{children}
</ModalContext.Provider>
)
}
在根組件外加上 ModalProvider,這樣一來所有組件都能調用 openModal
, closeModal
:
// App.tsx
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { ModalProvider } from '@/provider/ModalProvider'
export default function App() {
return (
<ModalProvider>
<View style={styles.container}>
// ...
</View>
</ModalProvider>
)
}
如果要在頁面中控制 Modal,只需要使用 useContext(ModalContext)
,但每個頁面都要 import context 和 useContext 再去獲取 openModal, closeModal 還是不夠方便,所以可以再寫一個 hook 來精簡:
// hooks/useModal.ts
import { useContext } from "react"
import { ModalContext } from "@/context"
export const useModal = () => {
const context = useContext(ModalContext)
return context
}
使用 hook 調用 Modal 方式:
import React from "react"
import { View, Button } from "react-native"
import { useModal } from "@/hooks/useModal"
export const Page = () => {
const { openModal, closeModal } = useModal()
return (
<View style={styles.container}>
<Button title="Delete" onPress={() => openModal('delete')} />
</View>
)
}
除了 Modal 以外,其他希望全局使用的組件也可以利用 Context + Provider 的方式實現。
在 Modal 開啟時彈出 Toast 是會被覆蓋在底下的,但又很常會遇到這樣的需求,該怎麼解決呢? (這邊使用的是 react-native-toast-message)
import Toast from 'react-native-toast-message'
import { ModalProvider } from '@/provider/ModalProvider'
export default function App() {
return (
<ModalProvider>
<View style={styles.container}>
// ...
</View>
<Toast />
</ModalProvider>
);
}
這個問題是無法用 zIndex 解決的,不過還有以下幾種方式可以解決這個問題:
<Toast />
(僅限 react-native-toast-message 的解法)
// Modal
export const CustomModal = ({ visible, onRequestClose }: CustomModalProps) => {
// ...
const showToast = () => {
Toast.show({
type: 'success',
text1: 'Hello',
text2: 'This is some something 👋'
});
}
return (
<Modal
animationType="fade"
transparent={true}
visible={visible}
>
<TouchableWithoutFeedback onPress={onBackdropPress}>
// ...
</TouchableWithoutFeedback>
<Toast />
</Modal>
)
}
// App.tsx
import React from 'react'
import { StyleSheet, View, LogBox } from 'react-native'
import Toast from 'react-native-toast-message'
import { ModalProvider } from './src/provider/ModalProvider'
export default function App() {
return (
<ModalProvider>
<View style={styles.container}>
// ...
</View>
<Toast />
</ModalProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ddd',
justifyContent: 'center',
alignItems: 'center'
}
})
不過就是需要注意會有多重 toast 的情況發生若在當前畫面聚焦 TextInput 後彈出 Modal,預期的情況是 TextInput 會自動失去焦點,但實際上 TextInput 並不會自動失焦(即不會觸發 onBlur),鍵盤因此無法自動收起,會擋住Modal中的內容。
Github issues 也有人提出了這個bug,但至今仍未修復。目前來說並沒有一個完美的解決方案,只能在開啟 Modal 前將 Keyboard 關閉。
import { Keyboard } from 'react-native'
const openModal = () => {
if (Keyboard.isVisible()) {
Keyboard.dismiss()
}
// ...
}